#!/usr/bin/env ruby
# -*- coding: us-ascii -*-
# Elizabeth Wiethoff 2008

require 'simulate18' if RUBY_VERSION < '1.8'
require 'find'
require 'digest/md5' ; DigestAlgo = Digest::MD5

dir = File.dirname(__FILE__)
require File.join(dir, '..', 'auxil', 'osdetect')
require File.join(dir, '..', 'monkey', 'slashpath')
require File.join(dir, '..', 'monkey', 'time')

########################################

hack_line_ending
hack_home

if RUBY_VERSION < '1.8'
  $PREFS = Hash.new
else
  require File.join(dir, '..', 'auxil', 'yamlstore')
  yaml_path = File.join('~', 'superpref', 'superelse.yaml')
  $PREFS = YAMLStore.new(yaml_path)
end

dflt_edir = ENV['HOME'] ; dflt_esubdirs = true
dflt_cdir = dflt_edir ; dflt_csubdirs = dflt_esubdirs

dflt_prunes = ['[\\/]\.\w', '\.app$', '/dev$', '/private$']

dflt_exts = %w[
  pdf wpd doc ppt pps rtf txt 
  psd gif png jpg jpeg tif tiff pict pct 
  html htm xhtml mht css js 
  db kp 
  mp3 mid wav aif aiff mov wmv mpg 
  rb py java scala pl pm plx sh 
  xml xslt dtd 
]

$PREFS['edir'] = dflt_edir  unless $PREFS.include?('edir')
$PREFS['esubdirs'] = dflt_esubdirs  unless $PREFS.include?('esubdirs')
$PREFS['cdir'] = dflt_cdir  unless $PREFS.include?('cdir')
$PREFS['csubdirs'] = dflt_csubdirs  unless $PREFS.include?('csubdirs')

$PREFS['prunes'] = dflt_prunes unless $PREFS.include?('prunes')
$PREFS['exts'] = dflt_exts unless $PREFS.include?('exts')

(p $PREFS ; puts) if $DEBUG

########################################

class DataHash < Hash
  def append(key, item)
    self[key] = Array.new unless self[key]
    val = self[key]
    val << item
  end
end

class Key
  def Key.make(path)  # can raise error
    DigestAlgo.new(File.open(path).read).digest
  end
end

########################################

class DupeFinder
  def initialize
    @data = DataHash.new
    @already = {}
    @dupes = []
    @data_table = []
    
    @prunes = /#{$PREFS['prunes'].map { |s| "(#{s})" }.join('|')}/
    puts @prunes.source if $DEBUG  # ([\/]\.\w)|(\.app$)|(/dev$)
    
    @exts = /\.(#{$PREFS['exts'].join('|')})$/i
    puts @exts.source if $DEBUG  # \.(wpd|pdf|doc|rtf|txt|html|htm)$
  end
  
  
  def walk(msg, dir, subs, &block)
    $stdout.sync = true
    puts "%s %s files %s . . ." % [msg, dir.slashpath, 
      subs ? 'and subdirectories ' : '']
    
    Dir["#{dir}/*"].each do |glob|
      Find.find glob do |path|
        Find.prune if !subs and File.directory?(path)
        Find.prune if path =~ @prunes
        next unless File.file? path
        next unless path =~ @exts
        
        begin
          yield path  # can raise error
        rescue StandardError => e
          puts "PROBLEM READING FILE #{path.slashpath}: #{e.inspect}"
        end
        Thread.pass  # allow GC to do its thing
      end
    end
    puts ; puts
    $stdout.sync = false
    
    (p @data.values ; puts) if $DEBUG
  end
  
  
  def look
    puts 'Finding duplicates  . . .' ; puts
    @data.each { |ignore, group| @dupes << group if group.size > 1 }
    @data.clear
    @already.clear
    GC.start
  end
  
  
  def report
    puts 'Found these duplicates:'
    sorted_dupes = @dupes.sort_by { |group| [-group.size, group[0]] }
    sorted_dupes.each do |group|
      puts group.slashpath
      puts '----------'
    end
    $stdout.flush
  end
end

########################################

class DupeFinder
  def find_em
    (p $PREFS ; puts) if $DEBUG
    
    walk('Examining', $PREFS['edir'], $PREFS['esubdirs']) do |path|
      puts "file #{path.slashpath}" if $DEBUG
      @data.append(Key.make(path), path)
      print '.'
      @already[File.expand_path(path)] = true
    end
    
    walk('Comparing', $PREFS['cdir'], $PREFS['csubdirs']) do |path|
      puts "file #{path.slashpath}" if $DEBUG
      unless @already[File.expand_path(path)]
        key = Key.make(path)
        print '.'
        next unless @data[key]
        @data.append(key, path)
      end
    end
    
    look
    report
    
    puts
    puts 'FINIS!'
    $stdout.flush
  end
end

########################################

class DupeFinder
  @@column_info = [
    Hash[:title, 'GNo',       :width,  25, :column, 0, :type, Integer], 
    Hash[:title, 'Directory', :width, 175, :column, 1, :type, String], 
    Hash[:title, 'FileName',  :width, 175, :column, 2, :type, String], 
    Hash[:title, 'ModTime',   :width, 150, :column, 3, :type, Time], 
    Hash[:title, 'FileSize',  :width, 100, :column, 4, :type, Integer], 
    Hash[:title, 'GSz',       :width,  25, :column, 5, :type, Integer], 
  ]
  
  def self.column_info ; @@column_info ; end
  
  def data_table
    sorted_dupes = @dupes.sort_by { |group| [group.size, group[0]] }
    @data_table = []
    (0...sorted_dupes.size).each do |i|
      sorted_dupes[i].each do |file|
        gno = i + 1
        dir = File.dirname file
        nam = File.basename file
        mod = File.mtime file
        siz = File.size file
        gsz = sorted_dupes[i].size
        @data_table << [gno, dir, nam, mod, siz, gsz]
      end
    end
    return @data_table
  end
end

########################################

if $0 == __FILE__
  usage = 'usage: superelse [examination_glob [comparison_glob]]'
  
  eglob = ARGV.empty? ? '.' : ARGV.shift
  cglob = ARGV.empty? ? eglob : ARGV.shift
  
  $PREFS['edir'] = eglob ; $PREFS['esubdirs'] = true
  $PREFS['cdir'] = cglob ; $PREFS['csubdirs'] = true
  (p $PREFS ; puts) if $DEBUG
  
  df = DupeFinder.new
  df.find_em
  #df.data_table.each { |row| p row }
  #DupeFinder.column_info.each { |x| p x }
end
